/*******************************************************************************
 * Copyright 2007, 2009 InnovaSoft SA http://www.innovasoft.biz/
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
 
package biz.innovasoft.nodb.impl;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.prevayler.foundation.DeepCopier;

import biz.innovasoft.nodb.CompressableHashtable;
import biz.innovasoft.nodb.DeleteValidator;
import biz.innovasoft.nodb.Deterministic;
import biz.innovasoft.nodb.Filter;
import biz.innovasoft.nodb.Repository;
import biz.innovasoft.nodb.RepositoryConfiguration;
import biz.innovasoft.nodb.RepositoryException;
import biz.innovasoft.nodb.StoreValidator;
import biz.innovasoft.nodb.UpdateValidator;
import biz.innovasoft.nodb.index.DateResolution;
import biz.innovasoft.nodb.index.IndexManager;
import biz.innovasoft.nodb.index.RepositoryIndexManager;
import biz.innovasoft.nodb.query.QueryCriteria;
import biz.innovasoft.nodb.query.QueryType;
import biz.innovasoft.nodb.query.Tuple;
import biz.innovasoft.nodb.util.Utils;

public class GenericRepository implements Repository {

	private static final long serialVersionUID = 1336567853084686573L;

	protected long objectId = 1;
	private Map<Long, Object> objects;
	private Map<String,Class<?>>storedClasses = new HashMap<String,Class<?>>();

	private RepositoryIndexManager manager = new RepositoryIndexManager();
	private transient RepositoryConfiguration config;
	
	protected ArrayDeque<Long> availableOids = new ArrayDeque<Long>();
	
	/*
	 * Validators
	 */
	private Map<Class<?>, Map<String,UpdateValidator>>updateValidator = new HashMap<Class<?>,Map<String,UpdateValidator>>();
	private Map<Class<?>, Map<String,StoreValidator>>storeValidator = new HashMap<Class<?>,Map<String,StoreValidator>>();
	private Map<Class<?>, Map<String,DeleteValidator>>deleteValidator = new HashMap<Class<?>,Map<String,DeleteValidator>>();
	
	/*
	 * Helpers 
     */
	private UpdateHelper updater = new UpdateHelper(this);
	private StoreHelper storer = new StoreHelper(this);
	private DeleteHelper deleter = new DeleteHelper(this);
	private IndexHelper indexer = new IndexHelper (this);
	
	public GenericRepository () {
	}
	
	public GenericRepository (boolean compressed) {
		if (compressed) {
			objects = new CompressableHashtable<Long,Object>(compressed);
		} else {
			objects = new HashMap<Long,Object>(16);
		}
	}

	public void initialize (RepositoryConfiguration config) {
		this.config = config;
		this.indexer.initialize();
	}
	
	protected IndexHelper getIndexHelper () {
		return indexer;
	}
	
	protected Map<Long, Object> getObjects () {
		return this.objects;
	}
	
	public RepositoryConfiguration getConfiguration () {
		return this.config;
	}
	
	protected RepositoryIndexManager getRepositoryIndexManager () {
		return this.manager;
	}
	
	public List<Tuple> getAll() {
		return getAll(false);
	}
	
	public List<Tuple> getAll(boolean copy) {
		List<Tuple> tuples = new ArrayList<Tuple>();
		Set<Long> ids = objects.keySet();
		for (Long id : ids) {
			if ( copy ) {
				try {
					tuples.add ( new Tuple (id, Utils.copyBean(objects.get(id))));
				} catch (Exception e) {
					throw new RuntimeException("Error copying bean.");
				}
			} else {
				tuples.add ( new Tuple (id, objects.get(id)));
			}
		}
		return tuples;
	}

	public List<Tuple> getAll(Class<?> clazz) {
		return getAll(clazz, false);
	}
	
	public List<Tuple> getAll(Class<?> clazz, boolean copy) {
		List<Tuple> tuples = new ArrayList<Tuple>();
		
		Set<Long> ids = objects.keySet();
		for (Long id : ids) {
			Object object = objects.get(id);
			if ( object.getClass().equals(clazz)) {
				if ( copy ) {
					try {
						tuples.add ( new Tuple (id, Utils.copyBean(object)));
					} catch (Exception e) {
						throw new RuntimeException("Error copying bean.");
					}
				} else {
					tuples.add ( new Tuple (id, object));
				}
			}
		}
		return tuples;
	}
	
	public List<Tuple> getAll(Class<?> clazz, int first) {
		return getAll(clazz, first, false);
	}
	
	public List<Tuple> getAll(Class<?> clazz, int first, boolean copy) {
		int count = 0;
		List<Tuple> tuples = new ArrayList<Tuple>();
		
		Set<Long> ids = objects.keySet();
		for (Long id : ids) {
			Object object = objects.get(id);
			if ( object.getClass().equals(clazz)) {
				if ( copy ) {
					try {
						tuples.add ( new Tuple (id, Utils.copyBean(object)));
					} catch (Exception e) {
						throw new RuntimeException("Error copying bean.");
					}
				} else {
					tuples.add ( new Tuple (id, object));
				}
				if (++count == first) {
					break;
				}
			}
		}
		return tuples;
	}

	public List<Tuple> getAllWithFilter (Class<?> clazz, Filter filter) {
		return getAllWithFilter (clazz, filter, false);
	}
	
	public List<Tuple> getAllWithFilter (Class<?> clazz, Filter filter, boolean copy) {
		List<Tuple> allTuples = getAll (clazz, copy);
		List<Tuple> tuples = new ArrayList<Tuple>();
		for (Tuple tuple : allTuples) {
			if (filter.match(tuple.value)) {
				tuples.add(tuple);
			}
		}
		return tuples;
	}
	
	public Long[] getOIds() {
		return (Long[])objects.keySet().toArray(new Long[0]);
	}

	public Object getByOId (Integer id) {
		return getByOId (id);
	}
	
	public Object getByOId (Integer id, boolean copy) {
		return getByOId (id, copy);
	}
	
	public Object getByOId(Long id) {
		return getByOId(id, false);
	}

	public Object getByOId(Long oid, boolean copy) {
		Object o = objects.get(oid);
		if (o == null) return null;
		if (copy) {
			Object n = null;
			try {
				n = DeepCopier.deepCopy(o);
			} catch (Exception e) {
				throw new RuntimeException("Error copying bean.", e);
			}
			return n;
		}
		return o;
	}

	public List<Tuple> getObjectsByOId(Collection<Long> ids) {
		return getObjectsByOId (ids, false);
	}

	public List<Tuple> getObjectsByOId(Collection<Long> ids, boolean copy) {
		List<Tuple> _objects = new ArrayList<Tuple>();
		for (Long id : ids) {
			Object o = getByOId(id, copy);
			if (o == null) continue;
			_objects.add( new Tuple (id, o) );
		}
		return _objects;
	}
	
	public List<Tuple> getObjectsByOId (Long []ids) {
		return getObjectsByOId (ids, false);
	}
	
	public List<Tuple> getObjectsByOId (Long []ids, boolean copy) {
		List<Tuple> _objects = new ArrayList<Tuple>();
		for (Long id : ids) {
			Object o = getByOId(id, copy);
			_objects.add( new Tuple (id, o) );
		}
		return _objects;
	}
	
	protected Map<String,Class<?>> getStoredClassesMap () {
		return this.storedClasses;
	}
	
	protected void addStoredClass (Class<?> clazz) {
		synchronized (storedClasses) {
			storedClasses.put(clazz.getCanonicalName(), clazz);
		}
	}
	
	protected void removeStoredClass (Class<?> clazz) {
		synchronized (storedClasses) {
			storedClasses.remove(clazz.getCanonicalName());
		}
	}
	
	public Long store(Object object) throws RepositoryException {
		return storer.store(object, INDEX_YES);
	}

	public Long store(Object object, int index) throws RepositoryException {
		return storer.store(object, index);
	}

	public Long[] store(Collection<?> objects, int index)
			throws RepositoryException {
		return storer.store(objects, index);
	}

	public int size() {
		return objects.size();
	}

	public int size(Class<?> clazz) {
		Collection<Object> objs = objects.values();
		int size = 0;
		for (Object obj : objs) {
			if (obj.getClass().getCanonicalName().equals(
					clazz.getCanonicalName())) {
				size++;
			}
		}
		return size;
	}

	protected Collection<Class<?>> getStoredClasses () {
		Map<String,Class<?>> stClasses = new HashMap<String,Class<?>>();
		for (Object val : objects.values()) {
			stClasses.put(val.getClass().getCanonicalName(), val.getClass());
		}
		return stClasses.values();
	}
	
	public IndexManager getIndexManager (Class<?> clazz) {
		IndexManager imanager = manager.getIndexManager(clazz);
		return imanager;
	}
	
	public IndexManager[] getIndexManagers () {
		return manager.getIndexManagers();
	}
	
	public void reindex() throws RepositoryException {
		indexer.reindex();
	}
	
	public void reindex(Class<?> clazz) throws RepositoryException {
		indexer.reindex(clazz);
	}
	
	public void reindex(IndexManager imanager) throws RepositoryException {
		indexer.reindex(imanager);
	}

	public Object delete (Integer oid)  throws RepositoryException {
		return deleter.delete (oid.longValue(), true);
	}
	
	public Object delete(Long oid) throws RepositoryException {
		return deleter.delete (oid, true);
	}
	
	public Object delete(Long oid, boolean indexNow) throws RepositoryException {
		return deleter.delete (oid, indexNow);
	}
	
	public void delete (Long[] oids) throws RepositoryException {
		deleter.delete(oids);
	}
	
	public void delete (Class<?> clazz) {
		deleter.delete(clazz);
	}

	public void createIndex(Class<?> clazz, String indexPath) {
		manager.createIndex(clazz, indexPath);
	}

	public void createIndex(Class<?> clazz, String indexPath, boolean allowNulls) {
		manager.createIndex(clazz, indexPath, allowNulls);
	}
	
	public void createIndex(Class<?> clazz, String indexPath, DateResolution resolution, boolean allowNulls) {
		manager.createIndex(clazz, indexPath, resolution, allowNulls);
	}

	public void removeIndex(Class<?> clazz) {
		manager.removeIndex(clazz);
	}
	
	public void removeIndex(Class<?> clazz, String indexPath) {
		manager.removeIndex(clazz, indexPath);
	}
	
	public Object update (Long oid, Object object) throws RepositoryException {
		return updater.update (oid, object, INDEX_YES);
	}
	
	public Object update(Long oid, Object object, int index)
			throws RepositoryException {
		return updater.update (oid, object, index);
	}

	public void update (Collection<Tuple> tuples) throws RepositoryException {
		updater.update(tuples, Repository.INDEX_YES);
	}
	
	public void update (Collection<Tuple> tuples, int index) throws RepositoryException {
		updater.update(tuples, index);
	}
	
	public Object executeDeterministic(Deterministic action, Object... args)
			throws Exception {
		return action.execute(this, args);
	}

	public void addUpdateValidator (String validatorName, Class<?> clazz, UpdateValidator validator) {
		Map<String,UpdateValidator>clsValidators = updateValidator.get(clazz);
		if (clsValidators == null) {
			clsValidators = new LinkedHashMap<String,UpdateValidator>();
			updateValidator.put(clazz, clsValidators);
		}
		clsValidators.put(validatorName, validator);
	}
	
	public void removeUpdateValidator (String validatorName, Class<?> clazz) {
		Map<String,UpdateValidator>clsValidators = updateValidator.get(clazz);
		if (clsValidators != null) {
			if (clsValidators.containsKey(validatorName)) {
				clsValidators.remove(validatorName);
				if (clsValidators.size() == 0) {
					updateValidator.remove(clazz);
				}
			}
		}
	}
	
	public void addStoreValidator (String validatorName, Class<?> clazz, StoreValidator validator) {
		Map<String,StoreValidator>clsValidators = storeValidator.get(clazz);
		if (clsValidators == null) {
			clsValidators = new LinkedHashMap<String,StoreValidator>();
			storeValidator.put(clazz, clsValidators);
		}
		clsValidators.put(validatorName, validator);
	}
	
	public void removeStoreValidator (String validatorName, Class<?> clazz) {
		Map<String,StoreValidator>clsValidators = storeValidator.get(clazz);
		if (clsValidators != null) {
			if (clsValidators.containsKey(validatorName)) {
				clsValidators.remove(validatorName);
				if (clsValidators.size() == 0) {
					storeValidator.remove(clazz);
				}
			}
		}
	}
	
	public void addDeleteValidator (String validatorName, Class<?> clazz, DeleteValidator validator) {
		Map<String,DeleteValidator>clsValidators = deleteValidator.get(clazz);
		if (clsValidators == null) {
			clsValidators = new LinkedHashMap<String,DeleteValidator>();
			deleteValidator.put(clazz, clsValidators);
		}
		clsValidators.put(validatorName, validator);
	}
	
	public void removeDeleteValidator (String validatorName, Class<?> clazz) {
		Map<String,DeleteValidator>clsValidators = deleteValidator.get(clazz);
		if (clsValidators != null) {
			if (clsValidators.containsKey(validatorName)) {
				clsValidators.remove(validatorName);
				if (clsValidators.size() == 0) {
					deleteValidator.remove(clazz);
				}
			}
		}
	}
	
	protected Map<String,UpdateValidator> getUpdateValidator (Class<?> clazz) {
		return updateValidator.get(clazz);
	}
	
	protected Map<String,StoreValidator> getStoreValidator (Class<?> clazz) {
		return storeValidator.get(clazz);
	}
	
	protected Map<String,DeleteValidator> getDeleteValidator (Class<?> clazz) {
		return deleteValidator.get(clazz);
	}
	
	public boolean equals(Object obj) {
		return EqualsBuilder.reflectionEquals(this, obj);
	}

	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}

	public QueryCriteria createCriteria(Class<?> clazz) {
		throw new UnsupportedOperationException("This method is implemented in the proxy");
	}

	public QueryCriteria createCriteria(Class<?> clazz, boolean stricted) {
		throw new UnsupportedOperationException("This method is implemented in the proxy");
	}
	
	public void takeSnapshot() {
		throw new UnsupportedOperationException("This method is implemented in the proxy");
	}

	public QueryCriteria createCriteria(Class<?> clazz, QueryType type) {
		throw new UnsupportedOperationException("This method is implemented in the proxy");
	}

	public QueryCriteria createCriteria(Class<?> clazz, boolean stricted,
			QueryType type) {
		throw new UnsupportedOperationException("This method is implemented in the proxy");	
	}
	
	public void close () {
		this.indexer.close();
	}
	
}
